Add automated milestone management with Versions.props detection#34686
Conversation
Adds a PowerShell script that detects and fixes incorrectly milestoned PRs and issues by comparing what shipped in each release tag against the expected milestone. Supports three modes: - Single PR: -PrNumber 33818 (auto-detects which release it shipped in) - Single tag: -Tag 10.0.50 (checks all PRs in that release) - All SRs: -AllSRs (scans all service releases) Dry-run by default; -Apply flag required to make changes. Includes 56 Pester unit tests for milestone mapping, normalization, and linked issue extraction logic. GitHub Action workflow supports workflow_dispatch for manual testing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34686Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34686" |
There was a problem hiding this comment.
Pull request overview
Adds automation to detect and correct “milestone drift” by mapping merged PRs (and linked issues) to the service release tag they actually shipped in, with an opt-in apply mode via workflow_dispatch.
Changes:
- Added a
Fix-MilestoneDrift.ps1PowerShell script to analyze PRs/releases using git tags andgh api, and optionally apply milestone updates. - Added a
fix-milestone-drift.ymlworkflow to run the script in dry-run (default) or apply mode. - Added
Fix-MilestoneDrift.Tests.ps1Pester tests for the script’s pure functions (mapping/matching/parsing).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| .github/workflows/fix-milestone-drift.yml | New workflow_dispatch entrypoint to run the drift script with PR/tag/apply inputs. |
| .github/scripts/Fix-MilestoneDrift.ps1 | Core implementation: tag↔milestone mapping, PR enumeration between tags, GitHub milestone/issue updates via gh. |
| .github/scripts/Fix-MilestoneDrift.Tests.ps1 | Pester unit tests for mapping/matching and linked-issue extraction helpers. |
- Remove -AllSRs parameter entirely (too dangerous for bulk runs) - Add hard cutoff: PRs merged before 2026-01-01 are always skipped - Script now only supports -PrNumber (single PR) and -Tag (single release) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tion, injection fix - Move Set-StrictMode inside dot-source guard so it doesn't leak into Pester or other callers when the script is dot-sourced for testing - Add Get-PrNumbersReachableFromTag to handle GA (first tag) detection in Find-TagContainingPr instead of skipping it - Validate -Tag exists in repo and warn if PR is not in the tag range when -Tag is manually provided in single-PR mode - Replace pwsh -Command string concatenation with pwsh -File and proper bash array to prevent argument/command injection via workflow_dispatch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ConvertFrom-Json in PowerShell 7 converts ISO 8601 dates to DateTime objects. Calling [datetime]::Parse() on an already-parsed DateTime converts it to a locale-dependent string first, which fails on non-US locales (e.g. '02/13/2026 20:18:56' is not parseable everywhere). Handle both DateTime and string cases, using InvariantCulture for string parsing. This fixes 51 silent failures in the 10.0.50 audit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new -CreateIssue switch that creates a GitHub issue in dotnet/maui with a formatted markdown table of all corrections. The workflow gets a corresponding create_issue checkbox input. The issue includes a summary table (PRs checked, corrections needed) and a full corrections table showing current vs expected milestones. Only creates the issue when corrections are found. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR #34686 — Multi-Model Code Review (Round 7 — Final)CI Status: Review ResultsReviewer 1: No issues found. Reviewer 2: One finding — pre-cutoff PR skips miscounted as errors in tag mode. Previously identified in round 6 and accepted as a follow-up (only affects manual tag-mode against pre-2026 releases; auto-trigger path is unaffected). Reviewer 3: One finding — missing word boundary in No new findings across any reviewer. Summary Across All Review Rounds
Test Coverage
Recommended Action✅ Approve — Seven rounds of 3-model review. All actionable findings fixed. No new issues found. Ready to merge. |
- URL pattern now requires fixing keyword (fixes/closes/resolves) before GitHub issue URLs, matching GitHub's actual auto-close behavior. Bare informational URLs like 'See https://...issues/17549' no longer produce false positives. - Deduplicate corrections by (ItemType, Number) so the same issue linked from multiple PRs (e.g. individual PR + Candidate Branch PR) is only corrected once. - Added 2 new tests for URL-only references (58 total, all passing). Before: 109 corrections (17 duplicates, 1 false positive) After: 90 corrections (0 duplicates, 0 false positives) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When main (.NET 10) merges into net11.0, all .NET 10 PR commit messages become reachable from .NET 11 tags. Without protection, running this script against .NET 11 tags would incorrectly re-milestone thousands of .NET 10 PRs. Fix: Check each PR's base.ref (merge target branch) and skip PRs that don't belong to the .NET version being analyzed: - main/inflight/darc branches → allowed only for MainBranch=main - netX.0 branches → allowed only when X matches MajorVersion - release/X.* branches → allowed only when X matches MajorVersion New -MainBranch parameter (default: 'main') tells the script which branch owns the tags. For .NET 11, use -MainBranch net11.0. Added 14 new Pester tests (72 total, all passing). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Single-PR mode no longer requires a tag to exist. Instead, it reads MajorVersion and PatchVersion from eng/Versions.props at the PR's merge commit SHA. This correctly handles: - Tagged PRs: 10.0.50 at merge time → .NET 10 SR5 - Untagged PRs: 10.0.60 at merge time → .NET 10 SR6 - .NET 11 PRs: 11.0.0 at merge time → .NET 11.0 GA Tag mode also auto-derives MajorVersion from the tag itself (e.g., -Tag 10.0.50 → MajorVersion=10), removing the need to pass it. The -MajorVersion parameter default of 10 is kept as a fallback for the tag search path only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sub-patches are distinct milestones, not aliases for the parent SR. 10.0.41 maps to '.NET 10 SR4.1', not '.NET 10 SR4'. If that milestone doesn't exist on GitHub, the script will fail explicitly rather than silently milestoning to the wrong SR. Removed Test-MilestoneMatch sub-patch folding (SR4.1 no longer matches SR4). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PRs merged to inflight/current have merge commits that may not be in the local clone's history. Now fetches the commit on demand before reading Versions.props. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uses pull_request_target (closed + merged) to trigger on every PR merge to main, net*.0, inflight/*, and release/* branches. Reads Versions.props at the merge commit to determine the correct milestone, then applies it to the PR and its linked issues. Uses -Apply (not dry-run) since this is the automated path. No -CreateIssue since individual PR milestoning doesn't need reports. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merged milestone-on-merge.yml into fix-milestone-drift.yml as a single
'Milestone Management' workflow with two trigger modes:
- pull_request_target (closed+merged): auto-sets milestone on every
PR merge using Versions.props at the merge commit
- workflow_dispatch: manual dry-run or tag reconciliation with inputs
for pr_number, tag, apply, and create_issue
Also fixes shell injection from code review: all inputs now flow
through env vars instead of inline ${{ inputs.* }} interpolation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove duplicate Get-PrInfo call in Invoke-AnalyzeSinglePr (was
fetching PR twice, wasting an API call and creating TOCTOU window)
- Add Write-Warning to Get-MainBranchForVersion when origin/main
is unreachable (was silently falling back to net{Major}.0)
- Add Write-Warning to Get-VersionFromGitRef when merge commit
Versions.props can't be read (was silently returning null)
- Handle GA tag in Invoke-AnalyzeRelease: use
Get-PrNumbersReachableFromTag when no previous tag exists
(was throwing 'Cannot determine previous tag')
- Guard PreviousTag access in Save-ReportJson with ContainsKey
check (consistent with Write-Report and New-GitHubIssue)
73 Pester tests pass. E2E validated for both tagged and untagged PRs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add contents: read to workflow permissions (required for checkout) - Add Write-Warning for MajorVersion/PatchVersion parse failures in Get-VersionFromGitRef (was silently returning null) - Initialize PrsSkippedWrongBranch in report hashtable, remove ?? 0 - Add test for 3-digit sub-patch: 10.0.101 → .NET 10 SR10.1 74 Pester tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove unreachable $alt search path in Find-MatchingMilestone (normalized loop already handles the .NET 10 SR5 ↔ .NET 10.0 SR5 case) - Add prs_skipped_wrong_branch to Save-ReportJson summary block (was tracked and displayed in console but missing from JSON) - Fix Get-AllMilestones pagination: break when data.Count < 100 instead of looping when == 100 (avoids one extra API call on exact multiples of 100) 74 Pester tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Get-LinkedIssues: support bare 'close' and 'resolve' forms to match GitHub's auto-close spec (was only matching closes/closed/resolves/resolved) - Get-MainBranchForVersion: distinguish version-mismatch (Write-Verbose) from read-failure (Write-Warning) instead of one misleading message - Invoke-ApplyCorrections: track failure count and throw after all attempts so CI surfaces failures instead of exiting 0 - Rename Test-IsSrTag → Test-IsReleaseTag (also matches GA tags) 75 Pester tests pass (1 new test for close/resolve bare forms). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Single-PR mode: gracefully warn and skip when milestone doesn't exist
yet (prevents red CI on every merge before milestone is pre-created)
- Tag mode: throw when PrsChecked=0 and Errors>0 (was misleadingly
showing 'All milestones correct!' when nothing was checked)
- Extend milestone normalization to cover GA format ('.NET 10.0 GA'
now matches '.NET 10 GA'), with 2 new tests
- Dedup AlreadyCorrect counter (issues linked from multiple PRs
were double-counted)
- Apply corrections throw propagates failure count to CI
77 Pester tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MajorVersion was always overwritten: tag mode derived it from the tag, single-PR mode derived it from Versions.props at the merge commit. The parameter default of 10 was a hardcoded value that would need updating. Now: both analysis functions derive Major from the tag string. The fallback path (Find-TagContainingPr) reads it from origin/main via new Get-CurrentMajorVersion helper. Zero configuration needed. 77 Pester tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
<!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Summary Follow-up to #34686. Adds preview/RC milestone support and release branch detection to the milestone drift fixer. ## Problem PRs merged to \`net11.0\` were not being milestoned because \`Versions.props\` on that branch always has \`PreReleaseVersionIteration=1\` regardless of which preview the PR actually ships in. The iteration is only bumped on release branches. ## Solution ### Release Branch Detection (Primary) New detection step checks release branches first using \`merge-base --is-ancestor\`. For each PR, it finds the **earliest** release branch containing the merge commit: | Release Branch | Milestone | |---|---| | \`release/10.0.1xx\` | \`.NET 10.0 GA\` | | \`release/10.0.1xx-sr5\` | \`.NET 10 SR5\` | | \`release/11.0.1xx-preview1\` | \`.NET 11.0-preview1\` | | \`release/11.0.1xx-preview3\` | \`.NET 11.0-preview3\` | | \`release/12.0.1xx-rc1\` | \`.NET 12.0-rc1\` | ### Detection Order 1. **Explicit \`-Tag\`** — if provided 2. **Release branches** — \`merge-base --is-ancestor\` against \`release/{Major}.0.1xx-*\` branches, earliest match wins 3. **Versions.props** at merge commit — fallback for PRs not yet on any release branch 4. **Tag range search** — last resort ### Preview/RC Milestone Mapping \`ConvertTo-Milestone\` now accepts optional pre-release label and iteration: | Input | Milestone | |---|---| | \`11.0.0 + preview + 3\` | \`.NET 11.0-preview3\` | | \`12.0.0 + rc + 1\` | \`.NET 12.0-rc1\` | | \`10.0.60\` (stable) | \`.NET 10 SR6\` (unchanged) | ## Validated Results | PR | Base | Current Milestone | Script Result | Method | |---|---|---|---|---| | #33524 | net11.0 | .NET 11.0-preview1 | .NET 11.0-preview1 ✅ | Release branch | | #33233 | net11.0 | .NET 11.0-preview1 | .NET 11.0-preview1 ✅ | Release branch | | #30132 | net11.0 | .NET 11.0-preview3 | .NET 11.0-preview3 ✅ | Release branch | | #33834 | net11.0 | .NET 11.0-preview3 | .NET 11.0-preview3 ✅ | Release branch | | #34214 | net11.0 | .NET 11.0-preview2 | .NET 11.0-preview3 ✅ | Release branch (drift caught!) | | #34945 | net11.0 | .NET 11.0-preview4 | preview1 (fallback) | Versions.props (no p4 branch yet) | | #34620 | main | .NET 10 SR6 | .NET 10 SR6 ✅ | Release branch | | #34047 | main | .NET 10 SR4.1 | .NET 10 SR5 ✅ | Release branch (drift caught!) | PR #34214 is a real drift example: milestoned preview2 by a human, but actually on the preview3 release branch. ## Test Suite 88 Pester tests (11 new): - 6 for \`ConvertTo-Milestone\` preview/RC mapping - 5 for \`ConvertBranchToMilestone\` (GA, SR, preview, RC, non-release) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Summary
Adds automated milestone management for PRs and issues. A single workflow handles both automatic milestoning on PR merge and manual tag-based reconciliation after releases ship.
Problem
When PRs merge, they often get milestoned for the wrong service release or not milestoned at all. The actual release a PR ships in depends on which Candidate PR carries the commits and when the SR branch is cut. This creates milestone drift that makes release queries and notes inaccurate.
Solution
Files
.github/scripts/Fix-MilestoneDrift.ps1.github/scripts/Fix-MilestoneDrift.Tests.ps1.github/workflows/fix-milestone-drift.ymlHow It Works
Version Detection
The source of truth is
eng/Versions.props. For single-PR mode, the script readsMajorVersionandPatchVersionat the PR's merge commit SHA to determine what version the branch was building when the PR merged.Milestone Mapping
010.0.0.NET 10.0 GA1-910.0.5.NET 10.0 SR1N010.0.60.NET 10 SR6N0+x10.0.41.NET 10 SR4.1Sub-patches are distinct —
.NET 10 SR4.1!=.NET 10 SR4. Script warns and skips gracefully if the milestone doesn't exist on GitHub yet.Milestone Normalization
GitHub milestones have inconsistent naming (
.NET 10.0 SR4vs.NET 10 SR4,.NET 10.0 GAvs.NET 10 GA). The script normalizes both SR and GA forms as equivalent.Branch Ownership Detection
Reads
<MajorVersion>fromeng/Versions.propsonorigin/main. No hardcoded version numbers — automatically handles version transitions whenmainmoves from .NET 10 to .NET 11.Merge-Up Protection
Checks each PR's
base.refto skip PRs from different .NET versions (prevents merge-up commits from causing incorrect milestoning):base.refmainnet11.0maininflight/*,darc/*net11.0release/10.*release/11.*Linked Issue Detection
Scans PR title and body for:
fix/fixes/fixed/close/closes/closed/resolve/resolves/resolved #Nhttps://github.com/dotnet/maui/issues/NBare informational URLs are ignored. Results are deduplicated.
Single Workflow, Two Triggers
Auto: On PR Merge (
pull_request_target)Triggers on every PR merge to
main,net*.0,inflight/*, orrelease/*. ReadsVersions.propsat the merge commit, sets the milestone on the PR and its linked issues. If the milestone doesn't exist yet on GitHub, warns and skips gracefully (no red CI).Manual: Tag Reconciliation (
workflow_dispatch)pr_numbertag10.0.60)applyfalsecreate_issuefalseSafety
workflow_dispatchrequires explicitapplycheckboxenv:vars, not inline interpolationInvoke-ApplyCorrectionsthrows on failure; CI goes redKnown Limitations
PatchVersion=0to.NET X.0 GA. It does not readPreReleaseVersionLabelorPreReleaseVersionIterationfromVersions.props, so PRs merged tonet11.0during the preview phase will not be automatically milestoned (the workflow gracefully skips with a warning). Preview milestone support is planned as a follow-up.-Tag X.0.0(the very first release tag) in manual mode walks the full git history, which could exhaust GitHub API rate limits on large repos.Test Suite
77 Pester unit tests covering all pure functions:
ConvertTo-MilestoneTest-MilestoneMatchFind-MatchingMilestoneFind-PreviousTagGet-LinkedIssuesGet-PatchVersionTest-IsReleaseTagTest-PrBelongsToVersionLocal Usage
Validated Results
PatchVersion=50at merge →.NET 10 SR5✅PatchVersion=60at merge →.NET 10 SR6✅PatchVersion=60✅10.0.50→ 78 PRs checked, 90 corrections, 0 duplicates, 0 false positives ✅